Skip to content

fix(codemod): keep request() result schemas the call still needs#2383

Merged
felixweinberger merged 2 commits into
mainfrom
fweinberger/codemod-keep-dynamic-result-schema
Jun 29, 2026
Merged

fix(codemod): keep request() result schemas the call still needs#2383
felixweinberger merged 2 commits into
mainfrom
fweinberger/codemod-keep-dynamic-result-schema

Conversation

@felixweinberger

Copy link
Copy Markdown
Contributor

The v1→v2 codemod's schema-param-removal rule dropped the result-schema argument from every request() / callTool() call whose second argument was an MCP-imported *Schema. For request() that is only safe when the method is provably a literal: schema-less v2 request() enforces the spec result schema for known methods and resolves undefined for unknown ones, so stripping the schema from a dynamic-method call site — the proxy/forwarder shape, request({ method, params }, ResultSchema) — silently changed forwarding semantics and broke verbatim relays.

Motivation and Context

Found by dry-running the migration on a real multi-tenant relay: 12+ test failures traced to this rewrite (non-conforming upstream results rejected locally, unknown-method results becoming undefined, conforming results re-serialized in schema key order). The migration guide's new gateway/proxy section (#2382) documents the workaround for already-migrated code; this fixes the cause.

request() now keeps its schema unless the request object literal carries a literal method (string or no-substitution template, unwrapping as const / satisfies / parentheses, and not followed by a spread that could override it at runtime). The generic passthrough ResultSchema is always kept — even for literal spec methods, dropping it would switch the call from passthrough validation to spec-schema enforcement. callTool() is unchanged: its v2 signature has no schema parameter, so the argument always goes.

How Has This Been Tested?

Nine new transform tests covering the proxy/forwarder shapes (shorthand dynamic method, variable method property, non-object-literal request, generic and aliased ResultSchema, as const, spread-after-method) and pins for the unchanged behavior (literal methods still drop, callTool() always drops). Full codemod suite: 399 passing. Typecheck and lint clean.

Breaking Changes

None — the codemod now rewrites strictly fewer call sites; the kept schemas compile in v2 (request() accepts any Standard Schema and the import-path transform routes the constants to @modelcontextprotocol/core).

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Once this publishes, the guide sentence added in #2382 ("the codemod may have dropped the schema argument there; restore it") can be softened to reference codemod versions before this fix.

@felixweinberger felixweinberger requested a review from a team as a code owner June 29, 2026 12:20
@changeset-bot

changeset-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0790655

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@modelcontextprotocol/codemod Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2383

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2383

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2383

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2383

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2383

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2383

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2383

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2383

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2383

commit: 0790655

Comment thread .changeset/lucky-poets-refuse.md Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — the spec-method guard and changeset wording from the earlier round are addressed in 67bfe3f; the remaining inline note about extending the carve-out to compat/union schemas (e.g. CompatibilityCallToolResultSchema) is a minor edge case, not a blocker.

Extended reasoning...

Overview

This PR is confined to the @modelcontextprotocol/codemod package: it tightens the v1→v2 schema-param-removal transform so request() only loses its result-schema argument when the method is a provably-literal spec method (and never when the schema is the generic passthrough ResultSchema), while callTool() behavior is unchanged. The diff also bumps the auto-generated versions.ts and adds a changeset plus nine new transform tests covering proxy/forwarder shapes, as const/template-literal unwrapping, spread-after-method, and the literal non-spec-method case.

Security risks

None. The change only affects a developer-run migration tool that rewrites source ASTs locally; it does not touch transports, auth, protocol handling, or any runtime SDK code path.

Level of scrutiny

Moderate. The transform logic involves AST analysis, but the change makes the codemod strictly more conservative — it rewrites fewer call sites, and every kept schema still compiles in v2. Failure modes are limited to suboptimal (but compiling) migrated code rather than runtime SDK breakage. The earlier review round's blocking issue (literal non-spec methods having their schema stripped) and the prose inaccuracy in the changeset are both addressed in the latest commit: the guard now checks SPEC_REQUEST_METHODS derived from the existing SCHEMA_TO_METHOD mapping, the changeset says schema-less request() throws a TypeError for non-spec methods, and a test pins the literal non-spec-method case.

Other factors

The remaining finding posted inline — that the ResultSchema-only carve-out doesn't extend to other broader-than-spec schemas like CompatibilityCallToolResultSchema — is a genuine but narrow edge case (explicit compat schema on a literal spec-method call talking to a 2024-10-07 server), surfaces as a clear runtime validation error rather than silent corruption, and was already the pre-existing behavior before this PR. Test coverage is thorough (399 codemod tests passing per the description), the changeset is a patch bump for the codemod only, and no public SDK API surface changes.

The schema-param-removal rule dropped the result-schema argument from every
MCP-schema-bearing request()/callTool() call. For request() that is only safe
when the method is provably a literal string: schema-less v2 request()
enforces the spec result schema for known methods and resolves undefined for
unknown ones, so stripping the schema from a dynamic-method call site (the
proxy/forwarder shape) silently changed forwarding semantics and broke
verbatim relays.

request() now keeps its schema unless the request object literal carries a
literal method (string or no-substitution template, unwrapping as-const/
satisfies/parens, and not followed by a spread that could override it), and
the generic passthrough ResultSchema is always kept. callTool() is unchanged:
its v2 signature has no schema parameter, so the argument always goes.
Review follow-up: a literal method proves nothing about spec-ness, and
schema-less v2 request() throws a TypeError for non-spec methods, so a
custom-method call site like request({ method: 'acme/search' }, Schema)
lost a schema the call still needs. The drop is now gated on the literal
appearing in the spec request-method set (SCHEMA_TO_METHOD values). Also
corrects the changeset and comments: unknown methods fail loudly with a
TypeError, they do not resolve undefined.
@felixweinberger felixweinberger force-pushed the fweinberger/codemod-keep-dynamic-result-schema branch from 67bfe3f to 0790655 Compare June 29, 2026 13:43
@felixweinberger felixweinberger merged commit 9f8ba61 into main Jun 29, 2026
19 checks passed
@felixweinberger felixweinberger deleted the fweinberger/codemod-keep-dynamic-result-schema branch June 29, 2026 14:00
@claude claude Bot mentioned this pull request Jun 29, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant